#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os, time, re
from google import genai
from google.genai import types
import openpyxl
from openpyxl.styles import PatternFill, Font, Alignment

# ═══════════════════════════════════════════════════════════
# BEÁLLÍTÁSOK
# ═══════════════════════════════════════════════════════════

GEMINI_API_KEY  = os.environ.get("GEMINI_API_KEY", "BLINDED_PLACEHOLDER")
GEMINI_MODEL    = "gemini-2.5-flash"

EXCEL_FAJL      = "data_extraction.xlsx"
PDF_MAPPA       = "pdf_cikkek"
KIMENET_EXCEL   = "Audit_Jelentes_v2.xlsx"

FUTTATÁSOK      = 9     # v6: 6 → 9
KUSZOB          = 7     # v6: 5 → 7
VARAKOZAS_MP    = 20
MAX_PROBA       = 3

# Ha ennyi vagy több futtatás "HIBA:" választ ad, figyelmeztetünk
HIBA_KUSZOB     = 3

# Szünet a futtatások között (rate limit csökkentés)
FUTTATÁSOK_KOZI_VARAKOZAS = 2   # másodperc

BIAS_SHEETS     = ['ROBINS-I', 'RoB 2', 'NOS', 'CASP']

OUTCOME_OSZLOPOK = {
    'O1': {'kategoria': 38, 'outcome': 39, 'M': 59, 'SD': 60, 'n': 61},
    'O2': {'kategoria': 43, 'outcome': 44, 'M': 74, 'SD': 75, 'n': 76},
    'O3': {'kategoria': 48, 'outcome': 49, 'M': 89, 'SD': 90, 'n': 91},
    'O4': {'kategoria': None, 'outcome': 53, 'M': 104, 'SD': 105, 'n': 106},
}
N_OUTCOMES_OSZLOP = 5

# v6: Data Extraction szekciók — (col_tol, col_ig, label)
# Az AI egyszerre csak egy szekciót lát, így fókuszáltabb marad.
DE_SZEKCIOK = [
    (1,   37,  "1. szekció: Azonosítás, tanulmánydesign, minta, módszertan (1–37. oszlopok)"),
    (38,  58,  "2. szekció: Kimenet leírók — kategóriák, mérőeszközök, validálás (38–58. oszlopok)"),
    (59, 119,  "3. szekció: Statisztikai eredmények — átlagok, SD, n, tesztstatisztika, ES, p-értékek (59–119. oszlopok)"),
]

gemini_client = genai.Client(api_key=GEMINI_API_KEY)

# ═══════════════════════════════════════════════════════════
# PROMPTOK
# ═══════════════════════════════════════════════════════════

DATA_ELLENOR_PROMPT = """Te egy szisztematikus review adatellenőre vagy. Megkapsz egy tanulmány kinyert adatainak egy részét és a tanulmány szövegét.

JELENLEGI SZEKCIÓ: {szekció_kontextus}

FELADATOD:
Keresd meg azokat a cellákat, ahol a kinyert adat NYILVÁNVALÓAN NEM EGYEZIK a PDF szövegével (pl. rossz szám, hiányzó adat, félreolvasott érték).

FONTOS SZABÁLYOK:
- Ha az adat helyes vagy nem dönthető el egyértelműen, NE jelezd hibának. Ne keress hibát, ahol nincs.
- Csak azt jelöld hibának, ami EGYÉRTELMŰEN és VITATHATATLANUL rossz a PDF szövege alapján.
- NE jelezd hibának az értelmezési különbségeket, csak a tényszerű tévedéseket (pl. rossz szám, felcserélt csoport, hiányzó explicit adat).
- Ha nincs egyértelmű hiba, írd pontosan ezt, és semmi mást: "Nincs hiba."

KIMENET FORMÁTUMA (ha van hiba):
[Oszlop: X] Fejléc: JELENLEGI → JAVÍTOTT
Indok: egy mondat.

=== KINYERT ADATOK ===
{adat_szoveg}
"""

BIAS_ELLENOR_PROMPT = """Te egy szisztematikus review módszertani ellenőre vagy. Megkapsz egy tanulmány Risk of Bias értékelését ({bias_tool}) és a tanulmány szövegét.

FELADATOD:
Keresd meg azokat a cellákat, ahol az ítélet (Low/Moderate/Serious/Critical/NI/High stb.) NYILVÁNVALÓAN NEM EGYEZIK azzal, amit a módszertani rész tartalmaz.

FONTOS SZABÁLYOK:
- Ha az ítélet helyes vagy nem dönthető el egyértelműen, NE jelezd hibának. Ne keress hibát, ahol nincs.
- Csak az egyértelműen és vitathatatlanul téves ítéleteket jelezd, amelyek közvetlenül ellentmondanak a PDF módszertani részének.
- Ha nincs egyértelmű hiba, írd pontosan ezt, és semmi mást: "Nincs hiba."

KIMENET FORMÁTUMA (ha van hiba):
[Oszlop: X] Fejléc: JELENLEGI → JAVÍTOTT
Indok: egy mondat.

=== KINYERT RISK OF BIAS ADATOK ({bias_tool}) ===
{adat_szoveg}
"""

FORMAZO_PROMPT = """Az alábbi oszlopokban programmatikusan igazolt hiba van: legalább {kuszob} független ellenőr jelezte ugyanezt a problémát {futtatások} futtatásból.
Fogalmazd meg tömören a végső hibajelentést az alábbi adatok alapján.

Minden hibához pontosan ezt a formátumot használd:
[Oszlop: X] Fejléc: JELENLEGI → JAVÍTOTT
Indok: egy mondat.

FONTOS: Az [Oszlop: X] számokat pontosan úgy írd, ahogy a bemenetben szerepelnek. Ne változtasd meg őket.

Ha a lista üres, írd csak: "Nincs hiba."

=== KONSZENZUSOS HIBÁK ===
{hiba_lista}
"""

# ═══════════════════════════════════════════════════════════
# OSZLOP MAGYARÁZATOK — szétválasztva lapok szerint
# ═══════════════════════════════════════════════════════════

DE_OSZLOP_MAGYARAZAT = {
    4:  "Research domain: az egyetlen megengedett kategória a listából",
    5:  "N outcomes: az egész szám, ahány kimenetelt a tanulmány mér",
    15: "Statistical model type: M/SD-based / Regression-based / SEM-based / Mixed",
    20: "Waldorf program type: Full=akkreditált Waldorf / Partial=Waldorf-ihlette / Inspired=egyes Waldorf elemek",
    38: "O1 Outcome category: Academic/Social-emotional/Creativity/Motivation/Motor & arts/Reading & literacy/Other",
    43: "O2 Outcome category: ugyanaz a lista mint O1-nél",
    48: "O3 Outcome category: ugyanaz a lista mint O1-nél",
    59: "O1 Waldorf M: csak numerikus érték (a Waldorf csoport átlaga ezen a kimeneten)",
    62: "O1 Control M: csak numerikus érték (a kontrollcsoport átlaga)",
    65: "O1 Test statistic: t / F / χ² / z értéke (pl. t=2.34)",
    68: "O1 Effect size: Cohen's d vagy más ES értéke, csak szám",
    71: "O1 p-value: szignifikancia-szint (pl. .034 vagy <.001)",
    72: "O1 Direction: W+ = Waldorf szignifikánsan jobb / = = nem szignifikáns / C+ = kontroll jobb / Mixed",
}

BIAS_OSZLOP_MAGYARAZAT = {
    3:  "D1 ítélet (Confounding): Low/Moderate/Serious/Critical/NI",
    5:  "D2 ítélet (Selection bias): Low/Moderate/Serious/Critical/NI",
    7:  "D3 ítélet (Classification): Low/Moderate/Serious/Critical/NI",
    9:  "D4 ítélet (Deviations): Low/Moderate/Serious/Critical/NI",
    11: "D5 ítélet (Missing data): Low/Moderate/Serious/Critical/NI",
    13: "D6 ítélet (Measurement): Low/Moderate/Serious/Critical/NI",
    15: "D7 ítélet (Reported result): Low/Moderate/Serious/Critical/NI",
    17: "Overall ítélet: a legsúlyosabb domain-ítélet alapján",
    19: "Harmonized: Low→Low / Moderate→Moderate / Serious vagy Critical→High",
}

# ═══════════════════════════════════════════════════════════
# SEGÉDFÜGGVÉNYEK
# ═══════════════════════════════════════════════════════════

_pdf_cache = {}

def pdf_feltolt(pdf_path):
    """PDF feltöltése a Gemini File API-ra (session cache)."""
    if pdf_path in _pdf_cache:
        return _pdf_cache[pdf_path]
    for proba in range(1, MAX_PROBA + 1):
        try:
            with open(pdf_path, "rb") as f:
                feltoltott = gemini_client.files.upload(
                    file=f,
                    config=types.UploadFileConfig(mime_type="application/pdf")
                )
            _pdf_cache[pdf_path] = feltoltott
            return feltoltott
        except Exception as e:
            print(f"    ⚠️ PDF feltöltési hiba ({proba}/{MAX_PROBA}): {str(e)[:60]}")
            time.sleep(5)
    return None


def gemini_hivas(prompt_szoveg, pdf_file=None):
    """Elemző hívás: temperature=1.0."""
    tartalom = []
    if pdf_file is not None:
        tartalom.append(types.Part.from_uri(
            file_uri=pdf_file.uri, mime_type="application/pdf"
        ))
    tartalom.append(prompt_szoveg)

    for proba in range(1, MAX_PROBA + 1):
        try:
            valasz = gemini_client.models.generate_content(
                model=GEMINI_MODEL,
                contents=tartalom,
                config=types.GenerateContentConfig(temperature=1.0)
            )
            return valasz.text.strip()
        except Exception as e:
            if "429" in str(e) or "quota" in str(e).lower():
                print(f"      ⏳ Kvóta, várakozás {VARAKOZAS_MP}s... ({proba}/{MAX_PROBA})")
                time.sleep(VARAKOZAS_MP)
            else:
                print(f"      ⚠️ Hiba: {str(e)[:60]}")
                time.sleep(5)
    return "HIBA: Nem sikerült választ generálni."


def gemini_hivas_gyors(prompt_szoveg):
    """Formázó hívás: temperature=0.0."""
    for proba in range(1, MAX_PROBA + 1):
        try:
            valasz = gemini_client.models.generate_content(
                model=GEMINI_MODEL,
                contents=prompt_szoveg,
                config=types.GenerateContentConfig(temperature=0.0)
            )
            return valasz.text.strip()
        except Exception as e:
            if "429" in str(e) or "quota" in str(e).lower():
                print(f"      ⏳ Kvóta, várakozás {VARAKOZAS_MP}s... ({proba}/{MAX_PROBA})")
                time.sleep(VARAKOZAS_MP)
            else:
                print(f"      ⚠️ Hiba: {str(e)[:60]}")
                time.sleep(5)
    return "HIBA: Nem sikerült választ generálni."


def oszlopszamok_kinyerese(valasz):
    """
    Egy ellenőr válaszából kinyeri az érintett oszlopszámokat.
    "Nincs hiba." és "HIBA:" esetén üres halmazt ad vissza.
    """
    if "nincs hiba" in valasz.lower():
        return set()
    if valasz.startswith("HIBA:"):
        return set()
    return set(int(m) for m in re.findall(r'\[Oszlop:\s*(\d+)\]', valasz))


def legjobb_leiras(oszlop, jelentesek):
    """
    Egy adott oszlopszámhoz visszaadja a leírást és javasolt javítást
    abból az ellenőr-válaszból, amelyik a legtöbb részletet tartalmazza.
    A regex DOTALL-lal fut, így a többsoros fejlécek sem vesznek el.
    """
    mintak = []
    for j in jelentesek:
        if j.startswith("HIBA:"):
            continue
        m = re.search(
            rf'(\[Oszlop:\s*{oszlop}\].*?Indok:[^\n]*)',
            j, re.IGNORECASE | re.DOTALL
        )
        if m:
            mintak.append(m.group(1).strip())
    return max(mintak, key=len) if mintak else f"[Oszlop: {oszlop}] (részletek nem kinyerhetők)"


def formazo_output_validal(vegso, elvart_oszlopok):
    """
    Ellenőrzi, hogy a formázó AI nem cserélte-e fel az [Oszlop: X] számokat.
    Ha eltérés van, fallback: nyers hiba_lista kerül a kimenetbe.
    Visszatér: (validalt_szoveg, ok: bool)
    """
    if "nincs hiba" in vegso.lower():
        return vegso, True

    talalt_oszlopok = set(
        int(m) for m in re.findall(r'\[Oszlop:\s*(\d+)\]', vegso)
    )
    if talalt_oszlopok != elvart_oszlopok:
        print(
            f"      ⚠️  Formázó validáció SIKERTELEN: "
            f"elvárt={sorted(elvart_oszlopok)}, talált={sorted(talalt_oszlopok)} "
            f"→ nyers lista használata"
        )
        return None, False
    return vegso, True


def hat_futtas_programmatikus(ellenor_prompt_fn, szekció_label=""):
    """
    9 független futtatás → programmatikus 7/9 konszenzus számlálás →
    csak a küszöböt elérő oszlopok kerülnek a végső jelentésbe.
    Az AI-t csak a végső szöveg formázásához használjuk.
    """
    if szekció_label:
        print(f"    📌 {szekció_label}")

    jelentesek = []
    for i in range(1, FUTTATÁSOK + 1):
        print(f"      {i}/{FUTTATÁSOK}...", end=" ", flush=True)
        v = ellenor_prompt_fn(i)
        jelentesek.append(v)
        if i < FUTTATÁSOK:
            time.sleep(FUTTATÁSOK_KOZI_VARAKOZAS)
    print()

    # API hiba detektálás
    hiba_valaszok = [j for j in jelentesek if j.startswith("HIBA:")]
    if len(hiba_valaszok) >= HIBA_KUSZOB:
        print(
            f"      ❌ FIGYELMEZTETÉS: {len(hiba_valaszok)}/{FUTTATÁSOK} futtatás API hibát adott! "
            f"Az eredmény megbízhatatlan lehet."
        )

    # Programmatikus számlálás
    oszlop_szamlalo = {}
    for j in jelentesek:
        for oszlop in oszlopszamok_kinyerese(j):
            oszlop_szamlalo[oszlop] = oszlop_szamlalo.get(oszlop, 0) + 1

    kuszob_oszlopok = {
        o: db for o, db in oszlop_szamlalo.items() if db >= KUSZOB
    }

    print(f"      📊 Szavazatok: { {o: f'{db}/{FUTTATÁSOK}' for o, db in sorted(oszlop_szamlalo.items())} }")
    print(f"      ✅ Küszöböt ({KUSZOB}/{FUTTATÁSOK}) elért: {sorted(kuszob_oszlopok.keys()) or 'nincs'}")

    if not kuszob_oszlopok:
        return "Nincs hiba."

    hiba_lista = "\n\n".join(
        f"{legjobb_leiras(o, jelentesek)}  [{db}/{FUTTATÁSOK} ellenőr jelölte]"
        for o, db in sorted(kuszob_oszlopok.items())
    )

    print(f"      ✏️  Formázás...", end=" ", flush=True)
    formazo_p = FORMAZO_PROMPT.format(
        kuszob=KUSZOB,
        futtatások=FUTTATÁSOK,
        hiba_lista=hiba_lista
    )
    vegso = gemini_hivas_gyors(formazo_p)
    print("Kész.")

    elvart_oszlopok = set(kuszob_oszlopok.keys())
    validalt, ok = formazo_output_validal(vegso, elvart_oszlopok)
    if not ok:
        return hiba_lista
    return validalt


def fejlec_tisztit(h):
    """Sortörések és felesleges szóközök eltávolítása a fejlécből."""
    if not h:
        return ""
    return re.sub(r'\s+', ' ', str(h)).strip()


def sor_szoveg(headers, row_values, oszlop_magyarazat, col_tol=1, col_ig=None):
    """
    Excel sor → szöveges lista az AI prompthoz.

    col_tol / col_ig: csak az ebbe az oszloptartományba eső cellák kerülnek bele.
    Az oszlopszámok a kimenetben mindig az eredeti Excel-számok (nem relatívak).
    """
    szoveg = ""
    for i, (h, v) in enumerate(zip(headers, row_values), start=1):
        if col_ig is not None and i > col_ig:
            break
        if i < col_tol:
            continue
        if h and v is not None and str(v).strip() not in ("", "None"):
            tiszta_fejlec = fejlec_tisztit(h)
            sor = f"[Oszlop: {i}] {tiszta_fejlec}: {v}"
            if i in oszlop_magyarazat:
                sor += f"  ← ({oszlop_magyarazat[i]})"
            szoveg += sor + "\n"
    return szoveg


def szekciok_jelentes_osszefuz(szekció_eredmenyek):
    """
    A 3 szekció eredményeit egyetlen szöveggé fűzi össze.
    A 'Nincs hiba.' válaszokat kiszűri, hacsak mind a 3 ilyen.
    """
    hibas = [e for e in szekció_eredmenyek if "nincs hiba" not in e.lower()]
    if not hibas:
        return "Nincs hiba."
    return "\n\n".join(hibas)


# ═══════════════════════════════════════════════════════════
# PROGRAMMATIKUS BIAS KONZISZTENCIA ELLENŐRZÉS
# ═══════════════════════════════════════════════════════════

def ertek_normalizal(v):
    """Kis/nagybetű és szóköz független összehasonlításhoz."""
    return str(v).strip().lower() if v else ""


def robins_harmonized_ellenor(ws, row_idx, study_id):
    """
    ROBINS-I: ellenőrzi, hogy
    1. Overall (col 17) logikusan következik-e D1–D7-ből
    2. Harmonized (col 19) helyesen van-e leképezve Overall-ból
    """
    hibak = []

    domain_itelet_oszlopok = [3, 5, 7, 9, 11, 13, 15]
    itelet_ertekek = []
    for col in domain_itelet_oszlopok:
        v = ertek_normalizal(ws.cell(row_idx, col).value)
        if v:
            itelet_ertekek.append(v)

    overall_val    = ertek_normalizal(ws.cell(row_idx, 17).value)
    harmonized_val = ertek_normalizal(ws.cell(row_idx, 19).value)

    if itelet_ertekek:
        if any(v == "critical" for v in itelet_ertekek):
            vart_overall = "critical"
        elif any(v == "serious" for v in itelet_ertekek):
            vart_overall = "serious"
        elif all(v == "low" for v in itelet_ertekek):
            vart_overall = "low"
        else:
            vart_overall = "moderate"

        if overall_val and overall_val != vart_overall:
            hibak.append(
                f"[ROBINS-I Overall, Oszlop 17] Elvárt: '{vart_overall}' "
                f"(D1–D7 alapján), talált: '{overall_val}'"
            )

    harmonized_map = {
        "low":      "low",
        "moderate": "moderate",
        "serious":  "high",
        "critical": "high",
        "ni":       "high",
    }
    if overall_val in harmonized_map:
        vart_harm = harmonized_map[overall_val]
        if harmonized_val and harmonized_val != vart_harm:
            hibak.append(
                f"[ROBINS-I Harmonized, Oszlop 19] Elvárt: '{vart_harm}' "
                f"(Overall='{overall_val}' alapján), talált: '{harmonized_val}'"
            )
    return hibak


def rob2_harmonized_ellenor(ws, row_idx, study_id):
    """
    RoB 2: ellenőrzi, hogy Harmonized (col 15) helyesen következik-e Overall-ból (col 13).
    Low→Low / Some concerns→Moderate / High→High
    """
    hibak = []
    overall_val    = ertek_normalizal(ws.cell(row_idx, 13).value)
    harmonized_val = ertek_normalizal(ws.cell(row_idx, 15).value)

    harmonized_map = {
        "low":           "low",
        "some concerns": "moderate",
        "high":          "high",
    }

    if overall_val in harmonized_map:
        vart_harm = harmonized_map[overall_val]
        if harmonized_val and harmonized_val != vart_harm:
            hibak.append(
                f"[RoB 2 Harmonized, Oszlop 15] Elvárt: '{vart_harm}' "
                f"(Overall='{overall_val}' alapján), talált: '{harmonized_val}'"
            )
    return hibak


def nos_harmonized_ellenor(ws, row_idx, study_id):
    """
    NOS: ellenőrzi, hogy Harmonized (col 19) helyesen következik-e Quality-ből (col 18).
    Good→Low / Fair→Moderate / Poor→High
    """
    hibak = []
    quality_val    = ertek_normalizal(ws.cell(row_idx, 18).value)
    harmonized_val = ertek_normalizal(ws.cell(row_idx, 19).value)

    harmonized_map = {
        "good": "low",
        "fair": "moderate",
        "poor": "high",
    }

    if quality_val in harmonized_map:
        vart_harm = harmonized_map[quality_val]
        if harmonized_val and harmonized_val != vart_harm:
            hibak.append(
                f"[NOS Harmonized, Oszlop 19] Elvárt: '{vart_harm}' "
                f"(Quality='{quality_val}' alapján), talált: '{harmonized_val}'"
            )
    return hibak


BIAS_KONZISZTENCIA_ELLENOR = {
    'ROBINS-I': robins_harmonized_ellenor,
    'RoB 2':    rob2_harmonized_ellenor,
    'NOS':      nos_harmonized_ellenor,
}


def fajlnev_meghat(study_id):
    """Study ID → PDF fájlnév."""
    try:
        return f"{int(study_id):02d}_tanulmany.pdf"
    except Exception:
        return f"{study_id}_tanulmany.pdf"


def n_outcomes_ellenor(ws, row_idx):
    """
    Ellenőrzi, hogy az N outcomes (E oszlop) egyezik-e
    a ténylegesen kitöltött O1-O4 outcome sorok számával.

    Ha n_deklaralt > 4, kihagyja az ellenőrzést: ilyenkor az 5. és
    további outcomes-ok az O4+ szabad szöveges mezőbe kerülnek, tehát
    a kitöltött slotok száma (max 4) szükségszerűen kisebb — ez nem hiba.
    """
    n_val = ws.cell(row_idx, N_OUTCOMES_OSZLOP).value
    try:
        n_deklaralt = int(str(n_val).strip()) if n_val else None
    except (ValueError, TypeError):
        n_deklaralt = None

    if n_deklaralt is None or n_deklaralt > 4:
        return None

    n_kitoltott = 0
    for ox, cols in OUTCOME_OSZLOPOK.items():
        outcome_col = cols['outcome']
        v = ws.cell(row_idx, outcome_col).value
        if v and str(v).strip() not in ("", "NR", "N/A", "None"):
            n_kitoltott += 1

    if n_deklaralt != n_kitoltott:
        return (
            f"[Oszlop: {N_OUTCOMES_OSZLOP}] N outcomes: "
            f"deklarált={n_deklaralt}, ténylegesen kitöltött outcome oszlop={n_kitoltott}. "
            f"Ellenőrizd az O1–O4 outcome cellákat."
        )
    return None


# ═══════════════════════════════════════════════════════════
# FŐPROGRAM
# ═══════════════════════════════════════════════════════════

def main():
    print("=" * 65)
    print(f"  WALDORF INTEGRÁLT LEKTOR v6  (gemini-2.5-flash, {FUTTATÁSOK}/{KUSZOB}, 3 szekció)")
    print("=" * 65)

    if not os.path.exists(EXCEL_FAJL):
        print(f"❌ Nem található: {EXCEL_FAJL}"); return

    wb_in = openpyxl.load_workbook(EXCEL_FAJL, data_only=True)

    if 'Data Extraction' not in wb_in.sheetnames:
        print("❌ 'Data Extraction' lap nem található!"); return
    ws_de      = wb_in['Data Extraction']
    de_headers = [ws_de.cell(3, c).value for c in range(1, ws_de.max_column + 1)]

    bias_headers = {}
    for sn in BIAS_SHEETS:
        if sn in wb_in.sheetnames:
            ws_b = wb_in[sn]
            bias_headers[sn] = [ws_b.cell(1, c).value for c in range(1, ws_b.max_column + 1)]

    # Kimenet Excel
    wb_out = openpyxl.Workbook()
    ws_out = wb_out.active
    ws_out.title = "Audit Riport"
    ws_out.append(["Study ID", "Fájlnév", "Típus", "Konszenzusos Hibajelentés"])
    for cell in ws_out[1]:
        cell.font = Font(bold=True, color="FFFFFF")
        cell.fill = PatternFill("solid", fgColor="2F5496")
    ws_out.column_dimensions['D'].width = 80

    study_ids = []
    for r in range(4, ws_de.max_row + 1):
        sid = ws_de.cell(r, 1).value
        if sid and str(sid).strip():
            study_ids.append((r, sid))

    print(f"✅ {len(study_ids)} tanulmány | {FUTTATÁSOK} futtatás/szekció | {KUSZOB}/{FUTTATÁSOK} küszöb | 3 szekció\n")

    feldolgozott = 0

    for row_idx, study_id in study_ids:
        fajlnev  = fajlnev_meghat(study_id)
        pdf_path = os.path.join(PDF_MAPPA, fajlnev)

        print(f"\n{'─'*65}")
        print(f"[Study {study_id}] {fajlnev}")
        print(f"{'─'*65}")

        if not os.path.exists(pdf_path):
            print("  ⏭️  PDF nem található, kihagyva.")
            ws_out.append([study_id, fajlnev, "—", "PDF nem található."])
            continue

        print("  📤 PDF feltöltése...", end=" ", flush=True)
        pdf_file = pdf_feltolt(pdf_path)
        if pdf_file is None:
            print("SIKERTELEN.")
            ws_out.append([study_id, fajlnev, "—", "PDF feltöltés sikertelen."])
            continue
        print("Kész.")

        # ── 1. N outcomes konzisztencia ellenőrzés ─────────────
        n_hiba = n_outcomes_ellenor(ws_de, row_idx)

        # ── 2. DATA EXTRACTION — 3 szekció ─────────────────────
        print("  📋 Data Extraction ellenőrzés (3 szekció)...")
        row_values = [ws_de.cell(row_idx, c).value for c in range(1, ws_de.max_column + 1)]

        szekció_eredmenyek = []
        for col_tol, col_ig, szekció_label in DE_SZEKCIOK:
            adat_szoveg = sor_szoveg(
                de_headers, row_values, DE_OSZLOP_MAGYARAZAT,
                col_tol=col_tol, col_ig=col_ig
            )
            if not adat_szoveg.strip():
                szekció_eredmenyek.append("Nincs hiba.")
                continue

            def de_prompt_fn(i, _adat=adat_szoveg, _label=szekció_label):
                p = DATA_ELLENOR_PROMPT.format(
                    szekció_kontextus=_label,
                    adat_szoveg=_adat
                )
                return gemini_hivas(p, pdf_file=pdf_file)

            eredmeny = hat_futtas_programmatikus(de_prompt_fn, szekció_label=szekció_label)
            szekció_eredmenyek.append(eredmeny)

        de_jelentes = szekciok_jelentes_osszefuz(szekció_eredmenyek)

        if n_hiba:
            de_jelentes = f"[N OUTCOMES ELTÉRÉS]\n{n_hiba}\n\n" + de_jelentes

        ws_out.append([study_id, fajlnev, "Data Extraction", de_jelentes])
        ws_out.cell(ws_out.max_row, 4).alignment = Alignment(wrapText=True)

        # ── 3. BIAS ellenőrzés ──────────────────────────────────
        for sheet_name in BIAS_SHEETS:
            if sheet_name not in wb_in.sheetnames:
                continue
            ws_b = wb_in[sheet_name]
            bh   = bias_headers[sheet_name]

            bias_row = None
            for r in range(2, ws_b.max_row + 1):
                if str(ws_b.cell(r, 1).value) == str(study_id):
                    if ws_b.cell(r, 3).value:
                        bias_row = r
                    break

            if not bias_row:
                continue

            print(f"  ⚖️  {sheet_name} ellenőrzés...")
            brow_values = [ws_b.cell(bias_row, c).value for c in range(1, ws_b.max_column + 1)]
            bias_adat = sor_szoveg(bh, brow_values, BIAS_OSZLOP_MAGYARAZAT)

            def bias_prompt_fn(i, sn=sheet_name, ba=bias_adat):
                p = BIAS_ELLENOR_PROMPT.format(bias_tool=sn, adat_szoveg=ba)
                return gemini_hivas(p, pdf_file=pdf_file)

            bias_jelentes = hat_futtas_programmatikus(bias_prompt_fn)

            konzisztencia_fn = BIAS_KONZISZTENCIA_ELLENOR.get(sheet_name)
            if konzisztencia_fn:
                konzisztencia_hibak = konzisztencia_fn(ws_b, bias_row, study_id)
                if konzisztencia_hibak:
                    konzisztencia_szoveg = "\n".join(
                        f"[LOGIKAI HIBA — programmatikusan észlelve]\n{h}"
                        for h in konzisztencia_hibak
                    )
                    bias_jelentes = konzisztencia_szoveg + "\n\n" + bias_jelentes
                    print(f"    ⚠️  {len(konzisztencia_hibak)} logikai hiba (Harmonized/Overall)")

            ws_out.append([study_id, fajlnev, sheet_name, bias_jelentes])
            ws_out.cell(ws_out.max_row, 4).alignment = Alignment(wrapText=True)

        wb_out.save(KIMENET_EXCEL)
        feldolgozott += 1
        print(f"  💾 Mentve → {KIMENET_EXCEL}")

    print(f"\n{'='*65}")
    print(f"  ✅ KÉSZ — {feldolgozott} tanulmány lektorálva.")
    print(f"  📂 Kimenet: {KIMENET_EXCEL}")
    print(f"{'='*65}\n")


if __name__ == "__main__":
    main()
